Skip to content

feat(api/v2): ✨ Implement showcase routes - #78

Open
SidneyNemzer wants to merge 4 commits into
BuildTheEarth:api/v2from
SidneyNemzer:api-v2/showcases
Open

feat(api/v2): ✨ Implement showcase routes#78
SidneyNemzer wants to merge 4 commits into
BuildTheEarth:api/v2from
SidneyNemzer:api-v2/showcases

Conversation

@SidneyNemzer

@SidneyNemzer SidneyNemzer commented Jan 27, 2026

Copy link
Copy Markdown
Contributor

Implements the showcase routes from #62.

Route Auth
GET /showcases public
GET /:teamId/showcases public, ?slug=true to look the team up by slug
POST /showcases, POST /:teamId/showcases team token
PUT /showcases/:id, PUT /:teamId/showcases/:id team token, owner only
DELETE /showcases/:id, DELETE /:teamId/showcases/:id team token, owner only

Reads are public because showcases are what the website puts on its landing page. Writes go through TeamScope, so a team can only touch its own showcases and a :teamId prefix has to name the authenticated team. approved is readable and filterable but deliberately not updatable: a team should not be able to approve itself onto the front page.

Creating a showcase takes either a multipart image or an existing uploadId, matching the create/link split v1 offers.

Image upload

v2 had no way to store an image, so this adds one:

  • common/db/external/s3.service.ts — a thin wrapper over the same CDN bucket the v1 API writes to. Missing credentials log a warning and fail calls with 503 rather than taking the app down.
  • common/uploads/uploads.service.ts — validates type, size and readability, stores the object, records the Upload row, and removes the object again if the row write fails. Deleting an upload only happens when no claim and no other showcase still points at it.

The blur placeholder is plaiceholder's base64 pipeline reimplemented on sharp, because plaiceholder v3 ships ESM only and api-v2 compiles to CommonJS. The recorded width and height are the real dimensions, unlike v1's fixed 1920x1080 that falls out of its resize call.

Known gap

Upload rows are not team-scoped in the schema, so a team can pass another team's uploadId and attach its image to their own showcase. v1 has the same hole in its link route. Closing it needs a schema change — an owner column on Upload, or a join back through the showcase or claim that created it — which is out of scope here.

Testing

160 tests pass, 34 of them new: unit specs for the controller, the service and UploadsService, plus a supertest pass over the real router covering the public/scoped split and the :teamId prefix mismatch. tsc --noEmit, nest build and eslint are clean apart from the 6 pre-existing unbound-method errors in the older specs.

Closes #62

@Nudelsuppe42

Copy link
Copy Markdown
Contributor

Nice!

I think your currently missing showcase.approved in the DTOs of the GET routes. The images might also need hash and checked

@Nudelsuppe42 Nudelsuppe42 moved this from Backlog to In Progress in @BuildTheEarth/web Tracker Mar 14, 2026
@Nudelsuppe42 Nudelsuppe42 added this to the API version 2 milestone Mar 14, 2026
SidneyNemzer and others added 3 commits August 27, 2026 18:08
Showcases need an image before they can be created, and v2 had no way to
store one. This adds the missing half: a thin S3Service over the CDN
bucket the v1 API already writes to, and an UploadsService that records
what it stored as an Upload row.

The blur placeholder is plaiceholder's base64 pipeline reimplemented on
sharp, because plaiceholder v3 ships ESM only and api-v2 compiles to
CommonJS. Unlike v1, the recorded width and height are the real ones
rather than the fixed 1920x1080 that fell out of its resize call.

Uploads are shared with claims, so deletion only removes a row nothing
else points at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The routes landed as drafts: nothing checked who was calling them, the
service was never registered as a provider, and creating a showcase
required an image that could not be uploaded yet.

Reads stay public, because showcases are what the website puts on its
landing page, and gain the `:teamId` prefix the roadmap asks for, with
the same `?slug=true` lookup v1 offers. Writes go through TeamScope, so a
team can only touch its own showcases, and `approved` is deliberately not
updatable: a team must not be able to approve itself onto the front page.

Creating a showcase now takes either a multipart image or an existing
uploadId, and deleting one takes its image with it unless a claim or
another showcase still uses it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kyanvde

kyanvde commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Picked this up and finished the two TODOs. Rebased onto the current api/v2 first — the branch was 5 commits behind and conflicted in app.module.ts.

Image upload — v2 had no way to store one, so the create route could never work:

  • common/db/external/s3.service.ts, a thin wrapper over the same CDN bucket v1 writes to. Missing credentials log a warning and fail calls with 503 instead of taking the app down.
  • common/uploads/uploads.service.ts validates type/size/readability, stores the object, records the Upload row, and rolls the object back if the row write fails.
  • The blur hash is plaiceholder's base64 pipeline reimplemented on sharp — plaiceholder v3 is ESM-only and api-v2 compiles to CommonJS. Recorded width/height are the real dimensions; v1 stores a fixed 1920x1080 that falls out of its resize call.

Authorization — reads stay public (showcases are what the landing page renders), writes go through TeamScope, so every route now exists both bare and behind :teamId as the roadmap asks. approved is readable and filterable but deliberately not updatable: a team should not be able to approve itself onto the front page.

Also fixed two things in the draft: ShowcasesService was never listed in the module's providers (it would not have booted), and POST now accepts either a multipart image or an existing uploadId, matching v1's create/link split.

One known gap, deliberately left in: Upload rows are not team-scoped in the schema, so a team can pass another team's uploadId and attach its image to their own showcase. v1 has the same hole in its link route. Closing it needs a schema change (an owner column on Upload, or a join through the showcase/claim that created it), which felt out of scope here.

Verified: 160 tests pass (34 new, including a supertest pass over the real router for the public/scoped split), tsc --noEmit clean, build clean, lint clean apart from the 6 pre-existing unbound-method errors.

Review feedback: the image DTO was missing `checked`, and `include: { image: true }`
returned whatever the Upload table happened to hold, including `claimId`.

Selecting the columns explicitly makes the response and ShowcaseImageDto the
same shape, so the docs cannot drift from what callers actually receive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kyanvde kyanvde changed the title Create showcase routes feat(api/v2): ✨ Implement showcase routes Aug 27, 2026
@kyanvde

kyanvde commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

@Nudelsuppe42 caught up on your review from January:

  • approved is now on ShowcaseDto, and it is filterable and sortable on both GET routes. It is not accepted on PUT though — a team approving its own showcases onto the front page seemed like the wrong default, so that stays a v1/admin action for now. Happy to add it if you'd rather have it here.
  • hash and checked are both on the image DTO now, along with width, height and createdAt.

While adding checked I swapped include: { image: true } for an explicit column select, so the response and ShowcaseImageDto are the same shape and the docs cannot drift from what callers actually get. That also drops claimId, which was being returned incidentally.

PR description is rewritten and the branch is rebased on current api/v2.

@kyanvde
kyanvde marked this pull request as ready for review August 27, 2026 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants